E2EEを支える安全な鍵管理 | Anonify解体新書4
前回の内容はこちら
TL;DR
セキュリティ・プライバシー保護技術Anonifyの主要技術要素について解説する連載記事(全8回)
Anonifyで使用されている技術要素を洗い出し重要な技術について簡単なサンプルプログラムを交えて解説する。
その4では「E2EEを支える安全な鍵管理」と題して、Enclave内の鍵データをシーリングしてEnclave外部に保存する方法ついて解説する。
サンプルプログラムはRustで開発
安全な鍵管理についてはデータのシーリングだけではなくVMの構成などインフラ面でも考慮することが多々あるのでインフラ周りについては次回解説予定
Anonifyではデータの秘匿性を担保するため、E2EEを使ってクライアントプログラムと通信をする。
E2EEの実装にはcrypto_boxとデータのシーリングを組み合わせる必要があり、Anonifyでもそのような実装になっている。
crypto_boxはデータの暗号化技術、データのシーリングは鍵の安全な管理
今回はデータのシーリングについて取り上げる
ここで使用するコードはすべて独立して動作するのでAnonify自体の知識やAnonifyの動作環境は不要
この記事の中で使用する図は特別な記載がない限り全て筆者が作成したもの
サンプルプログラムはこちら
Anonifyが使用している主な技術要素
TEE(Intel SGX)関連
OCall/ECall
Remote Attestation
crypto_box(NaCl)
データのシーリング <- 今回解説するのはここ
mutual-TLS
Blockchain関連
スマートコントラクト
Web3
E2EEのプライベートキーを複数のEnclaveアプリで共有したい
複数のEnclaveアプリを起動して動作させる場合、生成した鍵をどう管理するべきか
アプリごとにキーを生成したらクライアントがどの鍵を使えば良いかわからない
Enclave上で動作するアプリをスケールさせるためには鍵の共有が必要
Enclaveの中で生成した鍵を永続化すれば複数アプリ間で共有できる
Enclaveの中のデータを安全にEnclaveの外に出すためにデータをシーリングしてから外に出す
Enclaveの外に出せる=永続化できる
Enclaveの中のデータを永続化できればマシンが落ちたり再起動してもデータを復元可能
以下はKubernetes上にEnclaveアプリを複数配置するイメージ(PlantUML) https://scrapbox.io/files/6151959be090dc002466cae9.svg
Nodeを複数(VMを複数)配置する場合、鍵の共有ができない問題がある
上記図はあくまでVMが1台の時のみ有効
SealingされたデータはCPUごとに固有なのでVMを跨いだ共有が簡単にできない
次回この辺り深掘りしていく予定
Anonifyでもプライベートキーの永続化のためにシーリングを使用している
glassonion1.icon Anonifyは上記の図よりももう少し複雑な構成だったりする
データのシーリングとは
シーリング(Sealing)、Sealは封印するという意味
glassonion1.icon sealにはアザラシとかオットセイという意味もある
cipepser.icon ほー知りませんでした
Intel SGXによって保護されるメモリ領域はEnclave(飛び地)と呼ばれ、Enclaveの中のコードとデータは、プロセス内外の攻撃から厳重に保護される。
Enclaveは揮発性メモリなので、マシンが落ちたり再起動したらデータは消える。
SGXが保護してくれるはあくまでメモリ(揮発性メモリ)でありディスク(不揮発性記憶装置)ではない
データを安全に永続化するためにEnclaveの中のデータをシーリングしてからディスクに保存する
複数マシンで同じデータを共有したい場合、データを永続化する必要がある
シーリングはEnclaveアプリケーションがEnclave固有のキーを使ってデータを暗号化する機能
シーリングされたデータを復号することをアンシーリングと呼ぶ。
シーリング方式はMRENCLAVEポリシーとMRSIGNERポリシーの2種類がある
シーリングしたデータはたとえMRENCLAVEやMRSIGNERが同じでも他のマシンからアンシーリングすることができない
glassonion1.icon こちら追加検証が必要、k8sで複数Nodeにまたがる構成だとキーの共有するの大変だな。。。
シーリングに関して、以下の記事がわかりやすかった
2つのシーリングポリシー
シーリングにはMRENCLAVEポリシーとMRSIGNERポリシー2種類のポリシーが存在する。
MRENCLAVEポリシー(Seal to the Current Enclave)
MRENCLAVEベースのシーリング
Root Sealing KeyとMRENCLAVEを使ってデータをシーリングする。
MRSIGNERポリシー(Seal to the Enclave Author)
MRSIGNERベースのシーリング
Root Sealing KeyとMRSIGNERとEnclave.config.xmlのProdID(プロダクトID)の値を使ってデータをシーリングする。
プロダクトIDは開発者が自由に設定できる
EGETKEY命令の中で使用される
Root Sealing Key
EGETKEY命令の中で使用するCPU固有のキー値、CPUごとに値が異なる
ISVSVNとシーリング
SVNはThe Security Version Number of the Enclaveの略。開発者が設定する任意のSVNのことをISVSVNという(Enclave.config.xmlのISVSVNに設定する)
ISVSVNはEnclaveがインスタンス化されるときにCPUに保存される
ISVSVNの他にCPUSVNとConfig SVNもシーリング時に使用する
開発者が指定できるのはISVSVN
ISVSVNで指定されたバージョン以下のキーのみ取得できる
以前のバージョンで作成されたキーを取得することができる
cipepser.icon 低いversionって受け入れるんです?脆弱性へのパッチ目的で使われるのがISVSNVという理解なので、脆弱性のある過去のversionを扱えるの目的を達成できていない気がしました。
glassonion1.icon Intelのドキュメントによると脆弱性のあるバージョンのデータを移行させるために過去バージョンのみ受け入れるみたいな思想のようです。脆弱性のある過去バージョンから新規バージョンのデータを不正に取得させないためってことだと解釈してます。
MRENCLAVEとMRSIGNER
前々回のRemote Attestationの時に軽く触れたMRENCLAVEとMRSIGNERについてここで詳しく解説する MRENCLAVE(enclave identity)
エンクレーブ内に配置されるコードとコンフィグファイル(Enclave.config.xml)、それらが配置される予定の順序と位置、およびそれらのページのセキュリティプロパティを識別する単一の256ビットハッシュ(これら3つのことをEnclave Measurementという)
簡単にいうとビルド時のEnclaveイメージとコンフィグファイルから算出されたハッシュ値
Enclave MeasurementはIntelのドキュメントの用語
ビルド時にMRENCLAVEレジスタに書き込まれる
MRENCLAVEの値はCPUに依存しないためマシンが違っても、プラグラムとEnclave.config.xmlの内容が同じでsgx_signのバージョンが同じであれば同じ値を生成することができる
nrryuya.icon > 注: sealing key自体はCPUに依存する (sealing keyの元となるoriginal seaking key的なもの、それとMRENCLAVEを合体してsealing keyを作る)
glassonion1.icon シーリングの一番ややこしいポイントですね
プログラムが変わるとMRENCLAVEの値が変わるということは、プログラムにバグがあって修正したり機能追加した場合にMRENCLAVEの値が変わるということ、Enclave.config.xmlの設定を変更してもMRENCLAVEの値が変わる
MRSIGNER(signing identity)
開発者がビルド時に使用するプライベートキーから公開鍵を生成してそれをハッシュ化したもの
IntelのドキュメントにはThe Enclave Author’s Public Keyと書かれている
The Enclave Authorはプログラム開発者のこと
Enclave初期化時にMRSIGNERレジスタに書き込まれる
Enclave.config.xmlとシーリングの関係
MRENCLAVEポリシー
ISVSVN以外の値はすべてシーリングに影響する
MRSIGNERポリシー
ProdIDのみシーリングに影響する
Enclave.config.xmlの例
code: Enclave.config.xml
<EnclaveConfiguration>
<ProdID>0</ProdID>
<ISVSVN>0</ISVSVN>
<StackMaxSize>0x40000</StackMaxSize>
<HeapMaxSize>0x100000</HeapMaxSize>
<TCSNum>2</TCSNum>
<TCSPolicy>1</TCSPolicy>
<DisableDebug>0</DisableDebug>
<MiscSelect>0</MiscSelect>
<MiscMask>0xFFFFFFFF</MiscMask>
</EnclaveConfiguration>
シーリングの流れ
RDRAND命令を使用してランダム値を生成する
この値をKey IDとよぶ
EREPORTからISVおよび各種セキュリティバージョン番号(SVN)を取得する
SVNはバージョンのチェックに使う
SVN=Security Version Number
Keyリクエストを生成する(引数は以下)
詳細はsgx_types::sgx_key_request_tを参照のこと
Key Name(Seal Key固定)
Key Id(ランダムハッシュ)
Key Policy(MRENCLAVEまたはMRSIGNER)
ISVSVN、CPUSVN、Config SVN
Attribute Mask、Misc Mask
EGETKEY命令を使用してSealing Keyを取得する
引数はKeyリクエスト
指定されたKeyリクエストからCPUプロセッサ固有の値を使用してキーを取得する
Sealing Keyを使って暗号化する
暗号化アルゴリズムはRijndael128GCM
認証付き暗号の一種
何がCPU依存で何が非依存なのかわかりづらい問題
MRENCLAVEとMRSIGNERの値自体はCPU非依存
シーリングされたデータはCPU依存
とあるSGXマシンでシーリングされたデータを別のSGXマシンでシーリングすることはできない
EGETKEY命令を使って取得したキーの値はCPU固有になる
EGETKEY命令の詳細はこちら
どちらのシーリングポリシーを使うのが良いか
MRENCLAVEを使ったデータのシーリングは厳密ではあるものの柔軟性に乏しい
プログラムをアップデートをするとシーリング済みデータの復号ができなくなる
現実解はMRSIGNERポリシーな気がする
Rust SGX SDKのデフォルトはMRSIGNERポリシーが設定されている
glassonion1.icon AnonifyもMRSIGNERポリシーだった気がする
データをシーリングするプログラムをかいてみる
Enclaveの外から平文メッセージを渡してEnclaveの中でシーリングとアンシーリングをするサンプル
サンプルプログラムの構成
いつもの構成(Anonify解体新書1-3までと同じ)
code: sealeddata
sealeddata/ サンプルプログラムのディレクトリ
├ Makefile
├ app/
│ ├ Cargo.toml
│ ├ build.rs
│ └ src/
│ └ main.rs
├ enclave/
│ ├ Cargo.toml
│ ├ Enclave.config.xml
│ ├ Enclave.edl # EDL定義ファイル
│ ├ Enclave.lds
│ ├ Enclave_private.pem
│ └ src/
│ └ lib.rs
└ lib/ # 空でOKコンパイル後のファイルが入る
プログラムの全体像
非Enclave側からECallを介してデータをシーリングするプログラムを呼び出す
https://gyazo.com/be3119e70a63b6febe24b736c65374be
EDLの定義
データをシーリングするためのECall関数一つ
code: Enclave.edl
enclave
{
from "sgx_tstd.edl" import *;
from "sgx_stdio.edl" import *;
from "sgx_backtrace.edl" import *;
from "sgx_tstdc.edl" import *;
trusted
{
public sgx_status_t create_sealeddata(
size_t message_len
);
};
untrusted
{
};
};
Appのプログラム
init_enclaveしてから ECall関数create_sealeddataを呼び出すだけ
平文の文字列をECall関数に渡す
Enclave側で平文をシーリングする
code: main.rs
use sgx_types::*;
use sgx_urts::SgxEnclave;
static ENCLAVE_FILE: &'static str = "enclave.signed.so";
extern "C" {
fn create_sealeddata(
eid: sgx_enclave_id_t,
retval: *mut sgx_status_t,
message: *const u8,
message_len: usize,
) -> sgx_status_t;
}
fn init_enclave() -> SgxResult<SgxEnclave> {
... 省略 ...
}
fn main() {
let enclave = match init_enclave() {
... エラー処理省略 ...
};
let mut retval = sgx_status_t::SGX_SUCCESS;
// 平文の文字列をEnclaveに渡す
let msg = String::from("hello sealed data");
let result = unsafe {
create_sealeddata(
enclave.geteid(),
&mut retval,
msg.as_ptr() as *const u8,
msg.len(),
)
};
if result != sgx_status_t::SGX_SUCCESS {
println!("- ECALL Enclave Failed {}!", result.as_str()); return;
}
if retval != sgx_status_t::SGX_SUCCESS {
println!("- ECALL Enclave Failed {}!", retval.as_str()); return;
}
println!("+ sealeddata success..."); enclave.destroy();
}
Enclaveのプログラム(MRSIGNERポリシーでシーリング)
非Enclave側から渡ってきた平文の文字列をシーリングする
シーリングされたデータの情報をログに吐き出す
シーリングされたデータをアンシーリングしてもとの文字列に戻す
シーリングされたデータの情報を確認するためにSgxSealedData::to_raw_sealed_data_tを使う
code: lib.rs
extern crate sgx_tstd;
use sgx_tseal::SgxSealedData;
use sgx_tstd::{slice, str};
use sgx_types::{sgx_sealed_data_t, sgx_status_t};
const SEALED_LOG_SIZE: usize = 1024;
pub extern "C" fn create_sealeddata(message: *const u8, message_len: usize) -> sgx_status_t {
let message_slice = unsafe { slice::from_raw_parts(message, message_len) };
// 追加の認証データ(Additional Authentication data)
// MRSIGNERポリシーでデータをシーリングする
let result = SgxSealedData::<u8>::seal_data(&aad, &message_slice); let sealed_data = match result {
Ok(x) => x,
Err(ret) => {
return ret;
}
};
// シーリングしたデータを取得する
let p_sealed_log = sealed_log.as_mut_ptr();
let ret = unsafe {
sealed_data
.to_raw_sealed_data_t(p_sealed_log, SEALED_LOG_SIZE as u32)
};
if ret.is_none() {
return sgx_status_t::SGX_ERROR_INVALID_PARAMETER;
}
// シーリングしたデータの情報を出力する
unsafe {
println!(
"key_request.key_name: {}",
(*p_sealed_log).key_request.key_name
);
println!(
"key_request.key_policy: {}",
(*p_sealed_log).key_request.key_policy
);
println!("plain_text_offset: {}", (*p_sealed_log).plain_text_offset);
println!("payload_size: {}", (*p_sealed_log).aes_data.payload_size);
println!(
"payload_tag: {:?}",
(*p_sealed_log).aes_data.payload_tag.to_vec()
);
}
// シーリングしたデータからSgxSealedDataインスタンスを生成する
let opt = unsafe {
SgxSealedData::<u8>::from_raw_sealed_data_t(p_sealed_log, SEALED_LOG_SIZE as u32) };
let sealed_data = match opt {
Some(x) => x,
None => {
return sgx_status_t::SGX_ERROR_INVALID_PARAMETER;
}
};
// データをアンシーリングする
let result = sealed_data.unseal_data();
let unsealed_data = match result {
Ok(x) => x,
Err(ret) => {
return ret;
}
};
// アンシーリングされた平文を取得する
let data = unsealed_data.get_decrypt_txt();
// データの出力
println!("{:?}", data);
println!("{:?}", str::from_utf8(data).unwrap());
sgx_status_t::SGX_SUCCESS
}
実行結果
シーリングしたデータが無事にアンシーリングできていることを確認する
key_name=4
SGX_KEYSELECT_SEAL
key_policy=2
MRSIGNERポリシー
ちなみにMRENCLAVEポリシーの場合は1になる
code: bash
+ Init Enclave Successful 127435974639618! key_request.key_name: 4
key_request.key_policy: 2
plain_text_offset: 17
payload_size: 17
payload_tag: 179, 127, 251, 183, 115, 237, 167, 138, 78, 38, 112, 225, 238, 187, 47, 7 104, 101, 108, 108, 111, 32, 115, 101, 97, 108, 101, 100, 32, 100, 97, 116, 97 "hello sealed data"
Enclaveのプログラム(MRENCLAVEポリシーでシーリング)
SgxSealedData::seal_dataメソッドの代わりにSgxSealedData::seal_data_exメソッドを使用する
第一引数にMRENCLAVEを指定する
attribute_maskとmisc_maskはデフォルト値でOK
code: lib.rs
pub extern "C" fn create_sealeddata(message: *const u8, message_len: usize) -> sgx_status_t {
let message_slice = unsafe { slice::from_raw_parts(message, message_len) };
// attribute_maskにデフォルト値を設定する
let attribute_mask = sgx_types::sgx_attributes_t {
flags: sgx_types::TSEAL_DEFAULT_FLAGSMASK,
xfrm: 0,
};
// seal_data_exメソッドを使ってシーリングする
let result = SgxSealedData::<u8>::seal_data_ex( sgx_types::SGX_KEYPOLICY_MRENCLAVE, // MRENCLAVEポリシーを指定する
attribute_mask,
sgx_types::TSEAL_DEFAULT_MISCMASK,
&aad,
&message_slice,
);
... MRSIGNERポリシーと同じなので省略 ...
}
実行結果
key_policy=1
MRENCLAVEポリシー
code: bash
+ Init Enclave Successful 195880573468674! key_request.key_name: 4
key_request.key_policy: 1
plain_text_offset: 17
payload_size: 17
payload_tag: 214, 250, 107, 36, 0, 152, 229, 80, 110, 251, 122, 206, 206, 37, 31, 98 104, 101, 108, 108, 111, 32, 115, 101, 97, 108, 101, 100, 32, 100, 97, 116, 97 "hello sealed data"
参考: キーポリシーの定義
SGX_KEYPOLICY_MRENCLAVEとSGX_KEYPOLICY_MRSIGNER以外にも4つのポリシーが存在する
Enclave.config.xmlにセットした項目によってビットが立つ模様
ProdID以外にも設定項目がありそう
詳細は未調査
code: sgx_types/src/types.rs
/* Derive key using the enclave's ENCLAVE measurement register */
pub const SGX_KEYPOLICY_MRENCLAVE: uint16_t = 0x0001;
/* Derive key using the enclave's SINGER measurement register */
pub const SGX_KEYPOLICY_MRSIGNER: uint16_t = 0x0002;
/* Derive key without the enclave's ISVPRODID */
pub const SGX_KEYPOLICY_NOISVPRODID: uint16_t = 0x0004;
/* Derive key with the enclave's CONFIGID */
pub const SGX_KEYPOLICY_CONFIGID: uint16_t = 0x0008;
/* Derive key with the enclave's ISVFAMILYID */
pub const SGX_KEYPOLICY_ISVFAMILYID: uint16_t = 0x0010;
/* Derive key with the enclave's ISVEXTPRODID */
pub const SGX_KEYPOLICY_ISVEXTPRODID: uint16_t = 0x0020;
辛かったところ
SgxSealedData::to_raw_sealed_data_tメソッドに渡すデータは固定長バイナリ型じゃないとうまくアンシーリングできなかった
sgx_sealed_data_t型でいい感じにできないか試してみたものの無理だった
おまけ
この記事で紹介したコード以外に以下のサンプルを作成したので興味があればどうぞ。
シーリングしたデータをファイルに保存するサンプル
まとめ
E2EEを実現するにあたりSGXマシンのスケールを考慮すると鍵を複数マシンで共有する必要がある
複数のSGXマシンでデータを共有するためにはデータを永続化する必要がある
データのシーリングはEnclaveの中のデータを安全に永続化するための重要な機能
データのシーリングはMRENCLAVEポリシーとMRSIGNERポリシー、2種類存在する
MRENCLAVEポリシーはビルドごとにキーが変わるため実運用ではMRSIGNERポリシーの方が良さそう
シーリングしたデータを実際どうように保存すればいいのか、データを複数台のSGXマシンで共有するにはどうすればいいのかは次回解説する
今回はEnclaveの中で生成したデータをEnclaveの外部に出して保存する方法について解説しました。プログラムの更新時や複数のSGXマシンでデータを共有する場合にデータのシーリングはとても重要な機能になります。データのシーリングを通してMRENCLAVEとMRSIGNERについての深い学びがありました。今回データのシーリングについての解説に終始したため、じゃあ実際どうやってE2EEのプライベートキーを管理すればいいのというところまで書けませんでした。次回はインフラ構成を含めた実際の鍵管理について解説していきます。(文責・藤田) Anonify解体新書 | 連載一覧(全8回)